﻿Imports Microsoft.Xna.Framework
Imports Microsoft.Xna.Framework.Graphics

Public Class MovableObject
    Inherits GameObject

#Region " Fields "
    ' Constants
    Private Const mGravity As Single = 9.8F

    ' Phyiscs 
    Private mMass As Single
    Private mMomentum As Vector3 = Vector3.Zero
    Private mAcceleration As Vector3 = Vector3.Zero
    Private mVelocity As Vector3 = Vector3.Zero

    ' Forces and Effects flags
    Private mForcesAndEffectsToApply As ForcesAndEffects = ForcesAndEffects.Collision Or ForcesAndEffects.Gravity Or ForcesAndEffects.Heightmap ' Default value

    ' Object state
    Private mIsFalling As Boolean = True
    Private mIsActive As Boolean = True

#End Region ' Fields

#Region " Initialization "
    Public Sub New(ByVal GameManager As GameManager)
        MyBase.New(GameManager)
    End Sub

#End Region ' Initialization

#Region " Properties "
    Public Shared ReadOnly Property Gravity() As Single
        Get
            Gravity = mGravity
        End Get
    End Property

    ''' <returns>Vector3 with negative gravity as the Y component.</returns>
    Public Shared ReadOnly Property GravityVector3() As Vector3
        Get
            GravityVector3 = New Vector3(0.0F, -mGravity, 0.0F)
        End Get
    End Property

    ''' <returns>Vector3 with negative gravity as the Y component, adjusted for elapsed time.</returns>
    Public Shared ReadOnly Property GravityVector3(ByVal elapsedSeconds As Single) As Vector3
        Get
            GravityVector3 = New Vector3(0.0F, -mGravity * elapsedSeconds, 0.0F)
        End Get
    End Property

    ' TODO - set most? of these to readonly
    Public Property Mass() As Single
        Get
            Mass = mMass
        End Get
        Set(ByVal value As Single)
            mMass = value
        End Set
    End Property

    ''' <summary>
    ''' Mass * Velocity
    ''' </summary>
    ''' <remarks>TODO - Currently not really used.</remarks>
    Public Property Momentum() As Vector3
        Get
            Momentum = mMomentum
        End Get
        Set(ByVal value As Vector3)
            mMomentum = value
        End Set
    End Property

    ''' <summary>
    ''' Directional rate of change in speed. Measured in meters per second per second
    ''' Change in Velocity / time
    ''' </summary>
    Public Property Acceleration() As Vector3
        Get
            Acceleration = mAcceleration
        End Get
        Set(ByVal value As Vector3)
            mAcceleration = value
        End Set
    End Property

    ''' <summary>
    ''' Directional speed in metres per second.
    ''' Distance / time
    ''' </summary>
    Public Property Velocity() As Vector3
        Get
            Velocity = mVelocity
        End Get
        Set(ByVal value As Vector3)
            mVelocity = value
        End Set
    End Property

    Public Property IsFalling() As Boolean
        Get
            IsFalling = mIsFalling
        End Get
        Set(ByVal value As Boolean)
            mIsFalling = value
            If Not value Then
                Velocity = New Vector3(Velocity.X, 0.0F, Velocity.Y)
            End If
        End Set
    End Property

    Public Property IsActive() As Boolean
        Get
            IsActive = mIsActive
        End Get
        Set(ByVal value As Boolean)
            mIsActive = value
        End Set
    End Property

    Public Property ForcesAndEffectsToApply() As ForcesAndEffects
        Get
            ForcesAndEffectsToApply = mForcesAndEffectsToApply
        End Get
        Set(ByVal value As ForcesAndEffects)
            mForcesAndEffectsToApply = value
        End Set
    End Property
#End Region

#Region " Methods "

    Public Sub Accelerate(ByVal amount As Vector3)
        Acceleration += amount
    End Sub

    Public Overridable Shadows Sub Update(ByVal elapsedSeconds As Single, ByVal ccp As ICollisionCandidateProvider)
        If IsActive Then
            ApplyGravity(elapsedSeconds)

            ' Position to move to
            Dim pos As Vector3 = Position

            ' Calculate Velocity and Momentum
            Velocity += Acceleration * elapsedSeconds
            Momentum = Mass * Velocity

            Move(Velocity * elapsedSeconds)

            CheckHeighmap(elapsedSeconds, ccp)

            CheckForCollisions(elapsedSeconds, ccp, pos)
        End If

        MyBase.Update(elapsedSeconds)
    End Sub

    ''' <summary>
    ''' Check if gravity is being applied and object is currently falling.
    ''' 
    ''' If the object is already falling faster than the pull of gravity, 
    ''' then gravity will not affect acceleration.
    ''' 
    ''' Else decrease Y accleration by the appropriate portion of gravity.
    ''' </summary>
    Private Sub ApplyGravity(ByVal elapsedSeconds As Single)
        If (ForcesAndEffectsToApply And ForcesAndEffects.Gravity) = ForcesAndEffects.Gravity AndAlso IsFalling AndAlso -Gravity < Acceleration.Y Then
            Acceleration = Acceleration + GravityVector3(elapsedSeconds)
        End If
    End Sub

    ''' <param name="pos">Original position, before any moves</param>
    ''' <remarks>This is method is incomplete and not fully correct!</remarks>
    Private Sub CheckForCollisions(ByVal elapsedSeconds As Single, ByVal ccp As ICollisionCandidateProvider, ByVal pos As Vector3)
        ' If object should not be checked for collisions, then return
        If Not (ForcesAndEffectsToApply And ForcesAndEffects.Collision) = ForcesAndEffects.Collision Then Return

        ' List of objects that mave have collided with this object
        Dim collisions As List(Of GameObject) = ccp.GetCollisionCandidates(Me)
        If collisions.Count = 0 Then Return

        ' Direction object has moved
        Dim moveDirection As Quaternion = XNAObject.DirectionToPoint(pos, Position)


        ' Project a ray in the direction we have moved
        Dim closestCollisionRay As New Ray(pos, New Vector3(moveDirection.X, moveDirection.Y, moveDirection.Z))

        ' Max distance we can move along the ray. 
        ' Should start at the distance between pos and Position.
        Dim maxMoveDistance As Single = (pos - Position).Length

        ' Ensure it is a positive value
        maxMoveDistance = MathHelper.Max(maxMoveDistance, -maxMoveDistance)

        ' We will 'collide with' the closes object
        Dim collisionObject As GameObject = Nothing

        For Each gObj As GameObject In collisions
            ' Find the distance to the collision point
            Dim distance As Nullable(Of Single)
            gObj.BoundingSphere.Intersects(closestCollisionRay, distance)


            If distance.GetValueOrDefault < maxMoveDistance Then
                maxMoveDistance = distance.GetValueOrDefault
                collisionObject = gObj
            End If
        Next

        ' Move to point maxMoveDistance away from pos in direction moveDirection
        ' TODO - as this calculation is not yet corret
        ' MoveTo(pos + Vector3.Transform(pos, Matrix.CreateFromQuaternion(moveDirection * maxMoveDistance)), 1)

        If collisionObject IsNot Nothing Then CollideWith(collisionObject)
    End Sub

    ''' <summary>
    ''' If:
    ''' - object is supposed to be checked against a heighmap
    ''' - object is currently on the heightmap (within the X - Z coordinates)
    ''' - object is currently below the correct height (lower Y coordinate)
    ''' Then:
    ''' - Increase Y position.
    ''' </summary>
    ''' <param name="elapsedSeconds"></param>
    ''' <param name="ccp"></param>
    ''' <remarks></remarks>
    Private Sub CheckHeighmap(ByVal elapsedSeconds As Single, ByVal ccp As ICollisionCandidateProvider)
        If (ForcesAndEffectsToApply And ForcesAndEffects.Heightmap) = ForcesAndEffects.Heightmap _
            AndAlso ccp.HeightMapInfo.IsOnHeightmap(Position) Then

            Dim minimumHeight As Single = ccp.HeightMapInfo.GetHeight(Position)

            If Position.Y < BoundingSphere.Radius + minimumHeight Then
                PositionY = BoundingSphere.Radius + minimumHeight
                IsFalling = False
            End If
        End If

    End Sub

    Protected Overridable Sub CollideWith(ByVal gObj As GameObject)
    End Sub

#End Region ' Methods

End Class

<Flags()> _
Public Enum ForcesAndEffects
    Collision = 1
    Gravity = 2
    Bounce = 4
    Friction = 8
    Explosion = 16
    Lift = 32
    MagneticField = 64
    Heightmap = 128
End Enum